/** 
 * 节点类型，是用js模拟一个枚举
 */
const NodeType = {
    Start: 'start',
    End: 'end',
    Dialog: 'dialog',
    Perform: 'performance',
    Scene: 'scene',
    Comments: 'comments',
    Customize: 'customize'
}

const NodeColor = {
    'start': '#34D800',   // 绿色
    'end': '#A40004',     // 深红
    'dialog': '#FFAA00',  // 橙色
    'performance': '#6F0AAA', // 紫色
    'scene': '#61B7CF',     // 淡蓝
    'comments': '#AAAAAA',    // 灰色   
    'customize': '#476DD5'    // 蓝色
}

// 对话一行的高度
const lineheight = 24;

/**
 * 节点的基类
 */
class Node {
    /**
     * 
     * @param {int} x 
     * @param {int} y 
     * @param {NodeType} type 
     */
    constructor(x, y, type) {
        this.x = x;
        this.y = y;
        this.width = 240;
        this.height = 30;
        this.type = type;
        this.title = type;
        this.tag = null;
        this.selected = false;
        this.inputs = [];
        this.outputs = [];
        this.contents = [];
    }

    /**
     * 渲染函数
     * Node类只是负责在内存中的抽象表示，必须要调用渲染函数才能变成页面中的元素
     */
    draw() {
        if (this.selected) {
            drawRectStroke(this.x, this.y, this.width, this.height, 'white', 5);
        }

        // 显示的标题（标题加上标签）
        let viewTitle = this.title;
        if (this.tag != null) {
            viewTitle += ":" + this.tag;
        }
        // 标题栏
        drawRect(this.x, this.y, this.width, 30, NodeColor[this.type], viewTitle, true);    // 高度30是标题栏高度

        // 每个节点下面的对话
        if (this.contents.length == 0) return;
        // 每个对话的y轴起始
        let y = this.y + 30;
        ctx.font = "14px Arial"
        // 由于最后一个对话不用加分割线，所以单独提取出来
        for (let i = 0; i < this.contents.length - 1; i++) {
            const content = this.contents[i];
            drawRect(this.x, y, this.width, lineheight * content.lines, 'white',
                content.speaker + "：" + content.text, false);
            y += lineheight * content.lines;
            drawDevider(this.x + 4, y, this.width - 8, '#A0A0A0')
        }
        const lastContent = this.contents[this.contents.length - 1];
        drawRect(this.x, y, this.width, lineheight * lastContent.lines, 'white',
            lastContent.speaker + "：" +lastContent.text, false);
        ctx.font = "16px Arial";
    }

    /**
     * 添加标签，好在接收端进行特殊处理
     * @param {String} tag 
     */
    setTag(tag) {
        this.tag = tag;
    }

    /**
     * 添加输入节点
     * @param {Node} input 
     */
    addInput(input) {
        this.inputs.push(input);
    }

    /**
     * 添加输出节点
     * @param {Node} output 
     */
    addOutput(output) {
        this.outputs.push(output);
    }

    /**
     * 
     * @param {Content} content 
     * @param {int} index 插入的位置
     * @returns 
     */
    addContent(content, index) {
        // 如果插入位置在最后或者比最后还后，则直接push
        if (index >= this.contents.length) {
            this.contents.push(content);
            // 节点的高度加上这个对话的高度
            this.height += content.lines * lineheight;
            return;
        }

        // 如果是在中间或者最前，则新建数组插进去
        let contents = new Array(this.contents.length + 1);    // 提前分配好所需空间
        let j = 0;  // 这个是新数组的下标
        // 插入元素之前
        for (let i = 0; i < index; i++) {
            contents[j++] = this.contents[i];
        }
        // 插入元素
        contents[j++] = content;
        // 插入元素之后
        for (let i = index; i < this.contents.length; i++) {
            contents[j++] = this.contents[i];
        }        

        // 将新数组赋给老数组
        this.contents = contents;

        // 节点的高度加上这个对话的高度
        this.height += content.lines * lineheight;
    }

    /**
     * 
     * @param {int} index 要删除的对话的下标
     */
    deleteContent(index) {
        if (this.contents.length == 0) {    // 如果本来列表里就没有东西
            return;
        }

        const contents = new Array(this.contents.length - 1);
        let j = 0;
        for (let i = 0; i < this.contents.length; i++) {
            if (i != index) {
                contents[j++] = this.contents[i];
            }
        }
        this.contents = contents;
        this.updateHeight();
    }

    /**
     * 重新计算节点的高
     */
    updateHeight() {
        let height = 30;    // 30是标题栏默认的高
        for (const content of this.contents) {
            height += content.lines * lineheight;
        }
        this.height = height;
    }

    /**
     * 由于编辑菜单的种类有很多，则把创建编辑菜单的功能给节点自己
     * 如果想要重写编辑菜单，则重写此函数
     * @param {int} x 鼠标点击位置的x
     * @param {int} y 鼠标点击位置的y
     * @returns {EditMenu} 相应的编辑菜单
     */
    genEditMenu(x, y) {
        return new EditMenu(x, y, this);
    }
}

/**
 * 节点连线的挂载处
 */
class IOport {
    /**
     * 
     * @param {Node} parent 
     * @param {int} type 0代表输入，1代表输出
     * @param {int} xOffset 
     * @param {int} yOffset 
     */
    constructor(parent, type, xOffset = 10, yOffset = 10) {
        this.parent = parent;
        this.type = type;
        this.xOffset = xOffset;
        this.yOffset = yOffset;
    }

    draw() {
        let x = 0;
        let y = 0;
        if (this.type == 0) {
            x = this.parent.x + this.xOffset;
            y = this.parent.y + this.yOffset;
            drawRectStroke(x, y, 14, 14, 'black', 3);
            drawRect(x + 2, y + 2, 10, 10, 'green');
        } else {
            x = this.parent.x + this.parent.width - 24;
            y = this.parent.y + this.yOffset;
            drawRectStroke(x, y, 14, 14, 'black', 3);
            drawRect(x + 2, y + 2, 10, 10, 'green');
        }
    }
}

/**
 * 对话节点
 */
class DialogNode extends Node {
    constructor(x, y) {
        super(x, y, NodeType.Dialog);
        this.title = '对话';
    }

    genEditMenu(x, y) {
        const yOffset = y - this.y; // 点击点与节点最上面的y轴差值

        if (yOffset <= 30) {   // 如果点到的是标题栏，则触发者是节点
            return new EditMenu(x, y, this);
        }
        // 如果差值大于标题栏的高，则证明点到的是对话
        let lines = Math.ceil((yOffset - 30) / lineheight);   // 确认点击点在第几行（从1开始记的）
        let i = 0;  // i来记录点击到的对话的下标
        let curlines = 0  // 目前对话行数加起来
        for (; i < this.contents.length; i++) {
            curlines += this.contents[i].lines;
            if (curlines >= lines) {
                break;
            }
        }
        let selectedContent = this.contents[i];
        return new CustomEditMenu(x, y, selectedContent, [ 
                {
                    optionStr: "插入对话",
                    callback: () => {
                        // 先获取所需要的DOM元素
                        const inputfield = document.getElementById("inputfield");
                        const speakerInput = document.getElementById("speakerInput");
                        const contentInput = document.getElementById("contentInput");
                        // 再将对话框都给显示出来
                        inputfield.style.display = "block";
                        // 将对话框都清空
                        speakerInput.value = "";
                        contentInput.value = "";
                        _addContentIndex = i + 1;
                    }
                }, {
                    optionStr: "编辑对话",
                    callback: () => {
                        const inputfield = document.getElementById("inputfield");
                        const speakerInput = document.getElementById("speakerInput");
                        const contentInput = document.getElementById("contentInput");
                        const submitBtn = document.getElementById("contentSubmitBtn");

                        inputfield.style.display = "block";
                        speakerInput.value = selectedContent.speaker;
                        contentInput.value = selectedContent.text;
                        _editContent = selectedContent;
                        submitBtn.onclick = btnEditContentEvent;
                        // 剩下的事情在按钮点击事件
                    }
                }, {
                    optionStr: "删除对话",
                    callback: () => {
                        _selectedNode.deleteContent(i);
                    }
                }, {
                    optionStr: "删除节点",
                    callback: () => {
                        for (let i = 0; i < nodes.length; i++) {
                            if (nodes[i].selected) {
                                deleteNode(i);
                                break;  // 退出循环
                            }
                        }
                    }
                }
            ]
        );
    }
}

/**
 * 开始节点
 */
class StartNode extends Node {
    constructor(x, y) {
        super(x, y, NodeType.Start);
        this.title = '开始';
        this.height = 50;
        this.width = 160;
    }

    // 由于开始节点不能有输入，所以利用重载将此函数作废
    addInput(input) {
        return;
    }

    addContent(content, index) {
        return;
    }

    draw() {
        if (this.selected) {
            drawRectStroke(this.x, this.y, this.width, this.height, 'white', 5);
        }
        drawRect(this.x, this.y, this.width, this.height, NodeColor[this.type], this.title, true);
    }
}

/**
 * 结束节点
 */
class EndNode extends Node {
    constructor(x, y) {
        super(x, y, NodeType.End);
        this.title = '结束';
        this.height = 50;
        this.width = 160;
    }

    // 由于结尾节点不能有输出，所以利用重载将此函数作废
    addOutput(output) {
        return;
    }

    addContent(content, index) {
        return;
    }

    draw() {
        if (this.selected) {
            drawRectStroke(this.x, this.y, this.width, this.height, 'white', 5);
        }
        drawRect(this.x, this.y, this.width, this.height, NodeColor[this.type], this.title, true);
    }
}

/**
 * 演出节点
 */
class PerformNode extends Node {
    constructor(x, y) {
        super(x, y, NodeType.Perform);
        this.title = '演出';
    }

    // 演出不能选择，只能跳到下个节点，所以输出只能有一个
    addOutput(output) {
        this.outputs[0] = output;
    }
}

/**
 * 场景节点
 */
class SceneNode extends Node {
    constructor(x, y) {
        super(x, y, NodeType.Scene);
        this.title = '场景';
        // 场景是盛放别的节点的节点，inside数组负责存放别的节点
        this.inside = [];
    }

    /** 因为场景是没有输入输出的，所以将这个函数给重载废弃掉 */
    addInput(input) {
        return;
    }

    /** 因为场景是没有输入输出的，所以将这个函数给重载废弃掉 */
    addOutput(output) {
        return;
    }

    addContent(content, index) {
        return;
    }

    /**
     * 添加节点
     * @param {Node} node 
     */
    addNode(node) {
        this.inside.push(node);
    }

    genEditMenu(x, y) {
        return new CustomEditMenu(x, y, this, [{
                optionStr: "删除节点",
                callback: () => {
                    for (let i = 0; i < nodes.length; i++) {
                        if (nodes[i].selected) {
                            deleteNode(i);
                            break;  // 退出循环
                        }
                    }
                }
            }]
        );
    }
}

/**
 * 注释节点
 */
class CommentsNode extends Node {
    constructor(x, y) {
        super(x, y, NodeType.Comments);
        this.title = '注释';
    }

    /** 因为注释是没有输入输出的，所以将这个函数给重载废弃掉 */
    addInput(input) {
        return;
    }

    /** 因为注释是没有输入输出的，所以将这个函数给重载废弃掉 */
    addOutput(output) {
        return;
    }
}

/**
 * 自定义节点
 */
class CustomizeNode extends Node {
    constructor(x, y) {
        super(x, y, NodeType.Customize);
        this.title = '自定义';
    }
}


/**
 * 对话内容类，中文就简称为对话（“对话节点”则没有简称）
 */
class Content {
    /**
     * 
     * @param {String} text 内容
     * @param {String} speaker 说话对象（可以为空）
     * @param {int} linemax 一行最大字符数
     */
    constructor(text, speaker, linemax = 14) {
        this.text = text;
        this.speaker = speaker;
        this.lines = Math.ceil((speaker.length + 1 + text.length) / linemax);   // 这个成员对象表示行数,统计行数时加上“说话者:”
    }

    /**
     * 
     * @param {String} text 
     */
    setText(text, linemax = 14) {
        this.text = text;
        this.lines = Math.ceil((this.speaker.length + 1 + text.length) / linemax);
    }
}